Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notif: Get token on Android, and send to server #325

Merged
merged 7 commits into from
Oct 25, 2023

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Oct 17, 2023

This PR is stacked atop #319. → Rebased now that #319 is merged.

This implements part of #320.

To make an end-to-end demo, we also listen for notification messages, and just print them to the debug log.

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks; exciting to see this on its way! A few comments below from reading the code.

Comment on lines 51 to 52
// On a typical launch of the app (other than the first one after install),
// this is the only way we learn the token value; onTokenRefresh never fires.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I misunderstood "onTokenRefresh never fires" to mean that there isn't a possible case where the FCM system decides to replace the token during an app session (resulting in an onTokenRefresh call).

I think what you mean (since you do mention that case in an implementation comment in _onTokenRefresh) is that an onTokenRefresh call isn't part of the startup sequence except on the first startup after install.

I wonder if my misreading started with interpreting "on a typical launch of the app" instead as "in a session that started with a typical launch". Anyway, I wonder if there's an alternative wording that helps prevent a misreading like mine?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not quite that either — here's another way of putting what I mean here:

In a typical session of the app, onTokenRefresh never fires and our _onTokenRefresh callback is never called. In that case this code is the only way we learn the token value in that session.

That's "typical" in that the exceptions are the first time the app runs after install, and any time the token changes while the app is running. (And the latter is expected to be rare.)

The real point, though, is just that there are some cases where onTokenRefresh never fires and so we have to rely on this return value from getToken. As it happens, they're not even an edge case; they're the usual case.

@@ -451,6 +453,7 @@ class LivePerAccountStore extends PerAccountStore {
initialSnapshot: initialSnapshot,
);
store.poll();
store.registerNotificationToken();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the token-registration call want to go earlier than this, which comes after /register? For some users, /register will regularly take several seconds.

In particular, for the case where the user has already been getting and expecting notifications, if the device token has changed since the last run of the app, we have kind of an urgent task to try to register the new token so we don't unnecessarily drop notifications. (I see zulip-mobile doesn't do this earlier than /register success, but I wonder if that needs to be carried over to zulip-flutter.)

I see that we don't have our hands on a LivePerAccountStore (to call its registerNotificationToken) until after /register succeeds…hmm. But as a comment on LivePerAccountStore.load says, we might someday load a LivePerAccountStore from local storage, and that would let us call a LivePerAccountStore.registerNotificationToken sooner than the /register request.

…But actually, does the job of registerNotificationToken really need to wait until we have a LivePerAccountStore? (Does it need to be a method on LivePerAccountStore?) As implemented, it looks like its only dependencies are the NotificationService instance and an ApiConnection. The former is global, and for the latter we already have the value we need before starting the /register request. We may in the future need an ackedPushToken, to condition on whether the new token matches the old. But we can get that from the Account we already have our hands on before starting the /register request.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As implemented, it looks like its only dependencies are […]

I guess it rightly has another dependency: some means of tracking when the listener it adds on NotificationService.instance.token needs to be removed. That's a convenient role for LivePerAccountStore to fill…hmm. I see the "TODO call removeListener on [dispose]" in registerNotificationToken.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the token-registration call want to go earlier than this, which comes after /register?

Hmm, good thought. I think I basically just copied this aspect from zulip-mobile.

I guess it rightly has another dependency: some means of tracking when the listener it adds on NotificationService.instance.token needs to be removed. That's a convenient role for LivePerAccountStore to fill…hmm. I see the "TODO call removeListener on [dispose]" in registerNotificationToken.

Yeah. But that's really just because on dispose, we may be about to make a new LivePerAccountStore for the same account — so it's a way of preventing duplication. If the listener lived somewhere else that had a different lifetime, then we'd want to cancel it based on that thing's lifetime instead.

I guess there's the complication (which we'll eventually implement) that the user's API key could become invalid, or the user or realm get deactivated. In that case we'll want to tear down whatever has that listener, along with the LivePerAccountStore and/or whatever's doing the polling for events.

…But actually, does the job of registerNotificationToken really need to wait until we have a LivePerAccountStore? (Does it need to be a method on LivePerAccountStore?)

Yeah, I don't totally love having it here. It's here because it seems parallel to the event-polling loop poll; and because this is the most obvious place for the code to live if it's going to be initiated after we get the register response, i.e. from load.


Future<void> _registerNotificationToken() async {
final token = NotificationService.instance.token.value;
if (token == null) return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting; I guess this early return doesn't make things fall down if the get-device-token request happens to complete after the await _registerNotificationToken() (above) is called. That's because of this listener we add:

NotificationService.instance.token.addListener(_registerNotificationToken);

With that, it looks like we'll be sure to dispatch the register-with-server request as soon as we have a good result from the get-device-token request, which is good.

(And that behavior is covered in the tests; nice!)

Still, although that race is handled for users' benefit, I wonder if _registerNotificationToken and its public wrapper registerNotificationToken have an unnecessarily misleading return type. I think their callers can't tell by awaiting the Future whether the token registration succeeded, failed (as with an API error), or is still pending. Is that right? Maybe void as a return type is a better match for that behavior.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe void is the right return type given that.

I see that the tests use the Future<void> return type by awaiting the returned Future. Instead of doing that, I wonder if e.g. FakeAsync.elapse could be used instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this return type actually was void in a draft before I wrote the tests.

Instead of doing that, I wonder if e.g. FakeAsync.elapse could be used instead?

It's a bit tricky because in the "initially unknown" test case, we first want to wait for store.registerNotificationToken to complete whatever it's going to do, then check no request was made, and only after that wait for NotificationService.start to finish, including its getToken call.

I think a fully robust solution probably involves extending TestZulipBinding so that the test can control when getToken finishes.

}

Future<void> _getToken() async {
final value = await ZulipBinding.instance.firebaseMessaging.getToken(); // TODO(log) if null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging might be right for now, but I wonder about:

  • retrying
  • preparing to warn the user that setup failed or is not complete

I think this request will naturally fail in ordinary circumstances, like no Internet connection, no Google Play Services, etc., right?

I assume it might also hang indefinitely in some failure modes.

I think those could be TODOs for now though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, when it fails we'll want to note that for showing a warning (as in #323). Which, it's true, TODO(log) doesn't cover.

I assume it might also hang indefinitely in some failure modes.

Yeah, could be. We'll want #323 or related warnings to appear for the user if we haven't gotten a result here.

Comment on lines +286 to +354
LivePerAccountStore liveStore({Account? account, InitialSnapshot? initialSnapshot}) {
return LivePerAccountStore.fromInitialSnapshot(
account: account ?? selfAccount,
connection: FakeApiConnection.fromAccount(account ?? selfAccount),
initialSnapshot: initialSnapshot ?? _initialSnapshot(),
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I meant to put in my review (just posted now) but forgot: why should test code want a LivePerAccountStore? 🙂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, to test registerNotificationToken. 🙂 Or poll — we don't have tests for that, but probably should, and there's ways it should get more complex than it is and then those should definitely get tested.

This does mean that those two methods make LivePerAccountStore not really analogous to LiveZulipBinding or ApiConnection.live — unlike those, it contains nontrivial logic of its own that isn't just integrating with external side effects outside of Dart, logic that is amenable to unit tests. That definitely suggests there's some kind of refactoring to be done here that would draw the lines more cleanly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I guess that makes sense.

@gnprice
Copy link
Member Author

gnprice commented Oct 24, 2023

Thanks for the review! Revision pushed.

For several threads I stuck to adding TODO comments — there's refactors that would be good to make here, but I think this will work OK as it is, and I'd like to save those to circle back to after getting notifications working as a whole.

This will help keep things organized as we add more and more
features to the binding class, by letting the data and logic
for each feature live all in one contiguous section of the code.
This code is so trivial that I'm torn on whether the tests are
actually useful; they feel a lot like just repeating the implementation.
But they were easy to write.
This implements part of zulip#320.

To make an end-to-end demo, we also listen for notification
messages, and just print them to the debug log.
@chrisbobbe chrisbobbe merged commit 11d456d into zulip:main Oct 25, 2023
1 check passed
@chrisbobbe
Copy link
Collaborator

Thanks, LGTM! Merged.

@gnprice gnprice deleted the pr-notif branch October 25, 2023 19:22
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 12, 2024
…Token

The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 12, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 12, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 12, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 13, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 24, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 25, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 25, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 25, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 27, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Sep 28, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 7, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 7, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 7, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 7, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 8, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 8, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request Oct 11, 2024
The called method returns a not so useful future that is used for
testing.

See also:
  zulip#325 (comment)

Signed-off-by: Zixuan James Li <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants